Skip to content

fix(approval): preserve replacement routes during cleanup#4786

Open
samrusani wants to merge 3 commits into
tinyhumansai:mainfrom
samrusani:codex/4774-approval-waiter-drop-cleanup
Open

fix(approval): preserve replacement routes during cleanup#4786
samrusani wants to merge 3 commits into
tinyhumansai:mainfrom
samrusani:codex/4774-approval-waiter-drop-cleanup

Conversation

@samrusani

@samrusani samrusani commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Context

PR #4819 merged the core RAII waiter guard and closed #4774 after this PR was opened. This PR has been reconciled with current main and now contains only the remaining cleanup safeguards and regression coverage; it does not replace or duplicate the merged guard.

Remaining problem

  • Normal decision, channel-drop, timeout, and persistence-failure cleanup removed routes by thread/meeting key alone. An older approval finishing after a replacement approval had parked could therefore remove the replacement route.
  • The pending-row persistence failure path cleared the chat route but left an in-call meeting route pointing at an evicted waiter.
  • The merged coverage exercises chat cancellation and helper-level ownership, but not in-call cancellation or real old/new replacement interleavings.

Changes

  • Make normal and persistence-failure route cleanup conditional on the expected approval request ID, reusing the ownership-aware helpers introduced by fix(approval): RAII guard so a parked waiter is cleaned up on external turn teardown #4819.
  • Clear the in-call meeting route when pending-row persistence fails before returning the fail-closed denial.
  • Add end-to-end regressions for:
    • an older cancelled chat approval preserving and successfully deciding the replacement route;
    • an older cancelled in-call approval preserving and successfully deciding the replacement meeting route;
    • external in-call cancellation clearing its waiter, meeting route, and pending row;
    • pending-row storage failure clearing its waiter and meeting route.

Validation

  • env GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib openhuman::approval::gate::tests:: (36 passed)
  • cargo fmt --manifest-path Cargo.toml --all --check
  • env GGML_NATIVE=OFF cargo clippy -p openhuman --lib (existing warning baseline only)
  • git diff --check
  • Repository pre-push hook passed formatting, lint, frontend compile, Rust/Tauri check, and command-token guard.

Scope

One implementation/test file: src/openhuman/approval/gate.rs.

Related: #4774, #4819

Summary by CodeRabbit

  • Bug Fixes
    • Aborted or cancelled approval requests are now cleaned up reliably, including when parked approvals are cancelled externally.
    • Prevents stale chat-thread and in-call routing entries by clearing both mappings and using request-id–guarded cleanup, avoiding removal of newer routing.
    • Ensures a durable denial is delivered when parked approvals are interrupted/cancelled, with no lingering pending approvals.
  • Tests
    • Extended async coverage to verify external cancellation behavior and cleanup on persistence insert failures.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

ApprovalGate::intercept_audited now cleans up externally dropped parked approvals, including waiters, routing mappings, pending rows, and durable denial decisions. Normal completion disarms the guard, request-id checks protect newer routes, and tests cover chat and meeting routing.

Changes

Parked approval cleanup

Layer / File(s) Summary
Drop-based cleanup guard
src/openhuman/approval/gate.rs
ParkedApprovalCleanupGuard removes abandoned waiters and owned routing mappings, then persists a terminal Deny decision.
Interception lifecycle wiring
src/openhuman/approval/gate.rs
Failed pending-row persistence clears meeting mappings, allow outcomes preserve request ownership, normal cleanup clears both routes conditionally, and completion disarms the guard.
External-abort regression tests
src/openhuman/approval/gate.rs
Tokio tests cover persistence failure, external abort cleanup, durable denial, and preservation of newer chat and meeting routes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ApprovalFuture
  participant ApprovalGate
  participant ApprovalStore
  participant RoutingMappings
  ApprovalFuture->>ApprovalGate: park approval
  ApprovalGate->>ApprovalStore: insert pending approval
  ApprovalFuture->>ApprovalGate: external abort
  ApprovalGate->>RoutingMappings: clear owned routes
  ApprovalGate->>ApprovalStore: persist Deny
Loading

Suggested labels: bug

Poem

I’m a rabbit who guards every wait,
Dropped approval? I tidy the gate.
Threads vanish, meetings clear,
Deny is recorded, crisp and sincere.
Then hops the test suite—great!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements drop/external-teardown cleanup with waiter eviction, durable deny, and regression coverage for #4774.
Out of Scope Changes check ✅ Passed The changes stay within approval-gate cleanup and regression tests; no unrelated scope is evident.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: request-scoped cleanup that preserves replacement approval routes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@samrusani samrusani marked this pull request as ready for review July 12, 2026 07:58
@samrusani samrusani requested a review from a team July 12, 2026 07:58

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbcc54bc9a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/openhuman/approval/gate.rs Outdated
Comment on lines +251 to +252
self.gate.clear_thread(&self.thread_id);
self.gate.clear_meeting(&self.in_call_ctx);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid clearing newer approval routes

When an old parked approval is cancelled because a newer turn starts, the old task is only signalled and can be dropped after the new turn has already parked its own approval on the same thread/meeting. These key-only clears then remove whatever request is currently stored for that thread/meeting, so a fresh approval's yes/no reply no longer routes through pending_for_thread/pending_for_meeting and the new request can hang until TTL. Please remove the mapping only if it still points at this guard's request_id.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/openhuman/approval/gate.rs (2)

1400-1479: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid regression coverage for the external-abort cleanup path.

Both tests correctly validate waiter eviction, routing-map cleanup, and durable Deny persistence for chat- and meeting-routed parks. One gap worth considering: there's no test exercising the race this design is built to survive — decide() committing an Approve* decision concurrently with the abort, verifying the guard's store::decide(Deny) becomes a no-op (per the WHERE decided_at IS NULL conditional) rather than clobbering the already-persisted decision. That's the core correctness guarantee cited in the PR objectives ("conditional updates preserve concurrently committed decisions") and isn't directly covered here.

Want me to draft a test that calls gate.decide(&request_id, ApprovalDecision::ApproveOnce) immediately before handle.abort() and asserts the persisted decision stays ApproveOnce?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/approval/gate.rs` around lines 1400 - 1479, Add regression
coverage for the abort/decision race in the existing external-abort waiter
tests: commit an approval via gate.decide using the captured request_id
immediately before aborting the spawned waiter, then assert cleanup still occurs
and store::get_decision remains ApprovalDecision::ApproveOnce. Ensure the test
exercises the conditional Deny cleanup path so it cannot overwrite an
already-persisted approval.

206-271: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Blocking SQLite write inside Drop::drop can stall the runtime thread performing the cancellation.

Drop::drop (lines 237-270) calls store::decide(&self.gate.config, &self.request_id, ApprovalDecision::Deny) synchronously (line 254). Drop cannot yield, so whichever thread runs this destructor — plausibly a Tokio worker thread reclaiming the task during abort() or an outer-deadline drop — is blocked for the duration of the SQLite write/lock acquisition. Tokio's own docs call this out explicitly: "if you call a non-async method from async code, that non-async method is still inside the asynchronous context, so you should also avoid blocking operations there. This includes destructors of objects destroyed in async code."

This mirrors the existing synchronous store::decide calls at lines 842/862, but doing it in Drop is strictly worse: there's no way to bound it with a timeout or defer it, and it fires precisely during the failure scenario this PR targets (turn-deadline cancellation), which is exactly when many tasks might be torn down in a burst.

Consider offloading via tokio::task::spawn_blocking (fire-and-forget, logging errors from the spawned task) so the drop path never blocks its calling thread — or explicitly document that local SQLite writes are treated as acceptable "fast enough" blocking work here.

♻️ Possible direction (fire-and-forget via spawn_blocking)
         match store::decide(&self.gate.config, &self.request_id, ApprovalDecision::Deny) {
-            Ok(Some(_)) => tracing::debug!(...),
-            Ok(None) => tracing::debug!(...),
-            Err(err) => tracing::error!(...),
-        }
+        }
+        let config = self.gate.config.clone();
+        let request_id = self.request_id.clone();
+        tokio::task::spawn_blocking(move || {
+            match store::decide(&config, &request_id, ApprovalDecision::Deny) {
+                Ok(Some(_)) => tracing::debug!(request_id = %request_id, "..."),
+                Ok(None) => tracing::debug!(request_id = %request_id, "..."),
+                Err(err) => tracing::error!(request_id = %request_id, error = %err, "..."),
+            }
+        });

Note Config must be cheaply cloneable (or wrapped in Arc) for this to work without extra cost — worth confirming before adopting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/approval/gate.rs` around lines 206 - 271, Remove the
synchronous store::decide call from ParkedApprovalCleanupGuard::drop so
destructor execution never performs SQLite I/O. Preserve the existing in-memory
cleanup and denial behavior by cloning or otherwise safely capturing the
required config and request_id, then dispatching the denial persistence through
tokio::task::spawn_blocking with equivalent success and error logging inside the
spawned task; handle any task-spawn failure without blocking Drop.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/openhuman/approval/gate.rs`:
- Around line 1400-1479: Add regression coverage for the abort/decision race in
the existing external-abort waiter tests: commit an approval via gate.decide
using the captured request_id immediately before aborting the spawned waiter,
then assert cleanup still occurs and store::get_decision remains
ApprovalDecision::ApproveOnce. Ensure the test exercises the conditional Deny
cleanup path so it cannot overwrite an already-persisted approval.
- Around line 206-271: Remove the synchronous store::decide call from
ParkedApprovalCleanupGuard::drop so destructor execution never performs SQLite
I/O. Preserve the existing in-memory cleanup and denial behavior by cloning or
otherwise safely capturing the required config and request_id, then dispatching
the denial persistence through tokio::task::spawn_blocking with equivalent
success and error logging inside the spawned task; handle any task-spawn failure
without blocking Drop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 16d5574d-ba95-469e-bc01-db894687d36f

📥 Commits

Reviewing files that changed from the base of the PR and between d575f9d and dbcc54b.

📒 Files selected for processing (1)
  • src/openhuman/approval/gate.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

Addressed the replacement-route race in 648b8d8f5:

  • Thread and meeting cleanup now removes a route only when it still maps to that cleanup guard's request_id.
  • Applied the same request-ID guard to external-drop, pending-row persistence failure, and normal decision/timeout cleanup paths.
  • Added chat and in-call regressions that park a newer approval before aborting the older waiter and verify the newer route remains usable.

Validation:

  • env GGML_NATIVE=OFF cargo test --manifest-path Cargo.toml --lib openhuman::approval::gate::tests:: (33 passed)
  • cargo fmt --manifest-path Cargo.toml --all --check
  • env GGML_NATIVE=OFF cargo clippy -p openhuman --lib (existing warning baseline only)
  • git diff --check
  • repository pre-push hook passed

@codex review

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 13, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 648b8d8f56

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@samrusani samrusani changed the title fix(approval): clean up waiters dropped by turn timeouts fix(approval): preserve replacement routes during cleanup Jul 13, 2026

Copy link
Copy Markdown
Contributor Author

Reconciled this branch with current main in ed23921a7 after #4819 merged the core waiter guard.

The PR is now a one-file incremental follow-up:

  • route cleanup on normal and persistence-failure exits is request-ID scoped;
  • pending-row storage failure also clears the meeting route;
  • end-to-end chat/in-call replacement races, in-call external drop, and storage-failure cleanup are covered.

Validation: 36 approval-gate tests passed; fmt, clippy (existing warnings only), diff check, and the full repository pre-push hook passed. The PR is mergeable again and its title/body now describe only the remaining delta.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: ed23921a7e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Todo

Development

Successfully merging this pull request may close these issues.

Approval waiter/pending row leaks when a parked tool call is torn down by the turn wall-clock backstop

1 participant